Entrées/Sorties *************** La gestion des flux de données (clavier, fichiers, chaîne de caractères) constitue un incontournable en programmation. Des méthodes communes ===================== Héritage -------- Qu'est ce que l'héritage et à quoi sert-il dans notre contexte ? Que nous traitions des données qui arrivent d'un fichier, du clavier ou d'une chaîne de caractères, au final, quelle importance a réellement le support ? Ce qui nous intéresse, c'est le traitement des informations. Ici apparaît le besoin de mettre en place une abstraction nous permettant de traiter les données sans tenir compte du support. Cette abstraction s'appelle un **flux** (stream) de données. Pour la mise en place, on associe à chaque type de support un type d'objet spécifique. Mais ces objets différents vont proposer un ensemble de fonctions identiques (même nom et mêmes paramètres) pour gérer les flux de données. Du point de vue technique, cette abstraction a été réalisée en utilisant une hiérarchie de classes. Voici une représentation des classes de gestion des flux de la libraire standard du C++ : .. image:: heritage.png :scale: 70% La classe mère est la classe **ios** (input output stream). Elle offre des fonctions intéressantes comme : * `good() `_ : retourne true si le flux peut encore fournir des données. * `fail() `_ : retourne true si une erreur est survenue. La sous-classe de la classe **ios** spécialisée dans les flux en entrée est la classe `istream `_ (input stream). Elle offre diverses fonctionnalités comme la redéfinition de l'opérateur >>. Elle hérite des fonctions good() et fail() de sa classe mère et proposent de clases dérivées : * La sous-classe *istringstream* spécialisée dans la lecture des chaînes de caractères. * La sous-classe *ifilestream* spécialisée dans la lecture des fichiers. On trouve dans l'autre branche les classes symétriques pour la gestion des outputs : *ostream* et ses deux classes dérivées *ostringstream* et *ofilestream*. Vous pouvez aussi trouver une fonction globale de la librairie standard : **getline()**, prenant en paramètre tout objet de type *stream* ou ses dérivés. Cette fonction permet ainsi de lire une ligne aussi bien sur un flux clavier, un flux fichier ou flux texte, en entrée ou en sortie. On peut remarquer la présence de deux instances spécialisées sur les flux clavier entrée/sortie : **cout** et **cin**. L'instance *cout* (*cin*) est une instance de la classe *ostream* (*istream*) spécialisée dans le flux clavier sortie (entrée). Ces deux instances sont uniques pour chaque programme ce qui est normal car il y a une seule sortie et une seule entrée clavier associées à la fenêtre console de l'application. Les objets *cout* et *cin* sont instanciés avant le démarrage de votre programme. Ils sont accessibles par l'inclusion de la librairie ** dans votre fichier de code. Ainsi, vous pouvez les utiliser comme des objets préexistants sans que vous ayez à les instancier dans votre code. .. note:: Les exemples en bas des pages de cppreference.com sont assez pédagogiques, vous pouvez vous y référer pour voir comment mettre une méthode en pratique. .. note:: Les exemples que nous présentons sur cette page utilisent des chaînes de caractères en guise de données d'entrée. En effet, les chaînes de caractères sont plus faciles à mettre en place. Vous trouverez un exemple de mise en place d'un flux sur fichier à la fin de cette page. .. warning:: Si vous avez lu le chapitre sur les strings, vous savez que l'encodage des caractères spéciaux pose problème. Si vous traitez les flux par bloc en chargeant les données jusqu'à la fin de la ligne, les caractères spéciaux seront chargés sans problème. Si par contre vous traitez les flux caractère par caractère, vous aurez des problèmes dès que vous rencontrerez un *é* ou un *ç*. Dans tous les cas, si vos données n'utilisent que les caractères ASCII (voir page sur les strings) vous n'aurez aucun problème. Options de sortie ================= Écriture avec l'opérateur << ---------------------------- Si nous utilisons l'opérateur << avec l'objet *cout*, nous pouvons tout aussi bien l'utiliser pour écrire dans un fichier ou, chose étonnante, dans une chaîne de caractères. L'objet en question de type *ostringstream* accueille ainsi des écritures successives. Une fois les écritures terminées, on peut récupérer le résultat dans un objet de type string grâce à la fonction *str()*. Voici un exemple ci-dessous : .. code-block:: cpp #include #include #include using namespace std; int main() { ostringstream c; // output string stream int v = 10; c << "Var : " << v * 2 << " !"; string s = c.str(); // convertit stream => string cout << s << endl; } Le terme *endl* produit un retour à la ligne et reste équivalent à l'écriture de \"\\n\". On peut chaîner plusieurs affichages à la suite, cependant, les éléments sont accolés. Ainsi l'affichage de 18 et 13 produira 1813. Pour rendre lisible les résultats, il faut ainsi insérer des espacements supplémentaires : .. code-block:: cpp cout << "Bonjour, votre âge est de " << age << " ans." << endl; Formatage des nombres flottants ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ La librairie ** fournit une fonction *setprecision()* permettant de contrôler l’affichage des nombres flottants : .. code-block:: cpp #include #include using namespace std; int main() { cout << setprecision(5) << 3.141519 << endl; // ==>> 3.1415 5 chiffres cout << setprecision(3) << 3.141519 << endl; // ==>> 3.14 3 chiffres } Formatage des nombres entiers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ La librairie ** fournit une fonction *setw()* (set width) permettant d’indiquer la place que doit prendre un affichage. Ainsi, si un nombre prend 2 chiffres et que le largeur est fixée à 5, trois caractères seront rajoutés. La fonction *setfill()* permet d’indiquer le caractère servant de remplissage. Voici un exemple : .. code-block:: cpp #include #include int main() { std::cout << std::setfill('-'); std::cout << std::setw(5) << 25 << std::endl; std::cout << std::setw(5) << 148; } ==>> ---25 ==>> --148 Options de lecture ================== Nous listons ici différentes options pour la lecture. Le choix le plus souple et le plus polyvalent est sans aucun doute la fonction *getline()* qui s'applique sur tout objet de type *stream* et qui retourne un *string*. Le second choix que nous présentons correspond à l'opérateur >> offrant une syntaxe simple lorsque les données sont séparées par des espaces. Les autres exemples sont données à titre anecdotique. La fonction getline() --------------------- Une méthode fournissant une grande souplesse est la fonction *getline()*. Par défaut elle lit une ligne, c'est-à-dire qu'elle charge tous les caractères dans un objet string jusqu'à trouver un retour à la ligne. Mais, il est possible dans cette fonction de modifier le délimiteur de fin de ligne pour arrêter la lecture sur le caractère de son choix. Grâce à cette option, cela en fait un outil très pratique pour lire des informations séparées par des virgules ou par d'autres signes. Voici un exemple décryptant la chaîne de caractères : \"1,2,3,4;5,6,7,8;\" : .. code-block:: cpp #include #include #include using namespace std; // Le flux va être décodé en utilisant la fonction getline() // avec le caractère , comme délimiteur. // Ainsi, vont être lus successivement 1 2 3 et 4. // Convertis en valeurs entières, leur total est affiché. void ProcessLigne(istream & mystream) { string number; int somme = 0; while ( getline( mystream, number, ',') ) { cout << number << " - "; somme += stoi(number); } cout << endl; cout << "La somme est " << somme << endl << endl; } // On va d abord créer une string-stream mystream à partir des données. // Ensuite, grâce à la fonction getline() utilisant le caractère ; comme délimiteur, // nous allons lire la première ligne : 1,2,3,4 et la charger dans un objet string intitulé -ligne-. // On crée un deuxième string-stream L à partir de cet objet -ligne- et on le passe à la fonction ProcessLigne int main() { string data = "1,2,3,4;5,6,7,8;"; // traitement d'une string istringstream mystream; mystream.str(data); // lecture des blocs se terminant par un ; string ligne; while ( getline(mystream, ligne, ';') ) { cout << "Ligne : " << ligne << endl; istringstream L(ligne); ProcessLigne(L); } } Opérateur >> ------------ L'opérateur >> a été surchargé pour accepter sur sa gauche une instance de type *istream* (input stream) et sur sa droite une variable accueillant l'information lue. Ainsi, l'opérateur >> offre une syntaxe simple pour la lecture des données. Voici un exemple ci-dessous que vous pouvez tester : .. code-block:: cpp #include #include #include using namespace std; int main() { // traitement d'une input string stream istringstream s("1 2 3 4 5 6 7 8 9"); while ( s.good() ) { int a; s >> a; cout << a << " READ "; if (s.fail()) cout << " KO"; else cout << " OK"; cout << endl; } } .. note:: Cependant, l'opérateur >> est très contraignant car **ses délimiteurs sont imposés** : l'espace et le retour à la ligne. Si, vous devez lire des chiffres séparés par des points-virgules, vous ne pourrez donc pas l'utiliser. Si le rôle de l'opérateur >>, de la fonction *good()* et de la fonction *fail()* se comprennent facilement, nous ne connaissons pas leur comportement dans une situation non-standard. Pour cela, le mieux reste de mettre en place des tests pour déterminer leur logique de fonctionnement. Lancez le code précédent en modifiant la chaîne par la version ci-dessous. Examinez le comportement de l'opérateur >>, de la fonction *good()* et de la fonction *fail()*. .. code-block:: cpp istringstream s("1 2\n 3 4 5.6 7 8 9"); // \n retour à la ligne Le type de la variable de destination est utilisée par l'opérateur >> pour déterminer ce qu'il doit lire. Dans l'exemple précédent, changez le type de la variable *a* pour le type *double* et examinez comment se comporte l'opérateur >>. .. note:: Vous pouvez aussi proposer d'autres scénarios en modifiant la chaîne de caractères à votre guise. Les cas particuliers ne manquent pas : nombre trop long pour rentrer dans un int, nombre commençant par un 0, utilisation de la notation exponentielle, utilisation de la virgule ou du point pour les nombres flottants... Espacement fixe --------------- Dans certains fichiers, l'information occupe une largeur fixe. L'utilisation de la fonction *get(...,n)* permet de lire au plus *n-1* caractères et de les charger dans un tableau de *n* char. Il reste à construire l'objet string à partir du tableau de *char* pour ensuite le convertir vers le type désiré : .. code-block:: #include #include using namespace std; int main() { // formatage : 1er code 4 caractères, 2eme 8 caractères, dernier 4 caractères string k(" 11 1234 022"); std::istringstream s(k); cout << k << endl; // lit les 4 premiers caractères char t[5]; s.get(t,5); int a = stoi(string(t)); cout << a << endl; // lit les 8 caractères suivants char u[9]; s.get(u,9); int b = stoi(string(u)); cout << b << endl; // lit les 4 derniers caractères char v[5]; s.get(v,5); int c = stoi(string(v)); cout << c << endl; } Lecture d'un caractère ---------------------- Il est possible de lire un seul caractère. Cela peut parfois permettre de retrier des caractères spéciaux du flux comme dans la chaîne \"%%123%%\". Cette approche est généralement utilisée pour les entrées claviers, lorsque l'utilisateur doit entrer une valeur pour le choix d'un menu : .. code-block:: #include #include using namespace std; int main() { char c; cout << "(1) Choix 1" << endl; cout << "(2) Choix 2" << endl; cout << "(9) Quitter" << endl; while ( (c =cin.get()) != '9') { if ( c == '1') cout << "Choix 1" << endl; if ( c == '2') cout << "Choix 2" << endl; } } On peut trouver deux autres fonctions pratiques pour gérer un flux : * La fonction `peek() `_ qui lit le prochain caractère disponible sur le flux mais sans l'extraire. On peut par exemple ainsi vérifier si l'on a un chiffre ou une lettre pour déterminer le type du prochain élément à lire. * La fonction `ignore() `_ : qui exclut les prochains éléments sur le flux jusqu'à trouver un certain caractère. Points particuliers =================== Gestion des entrées/sorties fichier ----------------------------------- Pour gérer un fichier en entrée ou en sortie, il faut d'abord ouvrir un flux vers ce fichier et ensuite le refermer. Cela est important, car contrairement aux flux sous forme de *string* ou aux flux *cin* et *cout* qui appartiennent exclusivement à votre programme, les fichiers eux sont accessibles par tous. Ainsi, lorsqu'un programme demande l'accès à un fichier, celui-ci est verrouillé et ainsi on ne peut plus, par exemple, modifier son nom dans l'explorateur de fichier. Il faut attendre que le flux soit refermé pour pouvoir à nouveau modifier son nom ou le déplacer. Voici donc un exemple pour ouvrir et fermer un flux associé à un fichier : .. code-block:: cpp #include #include using namespace std; int main() { // ecriture en sortie sur un fichier ofstream F; F.open("c:\\temp\\test.txt"); F << "bonjour"; F.close(); } .. note:: Lorsque l'on écrit des données sur le disque, deux choix sont possibles. Soit on est écrit en mode standard ou en mode binaire. Lorsque le mode standard est choisi, les données sont écrites au format texte, ainsi un entier valant 123456 sera transcrit 123456 dans le fichier. Un fichier au format texte peut être ouvert et modifié dans n'importe quel éditeur de texte. Cependant, le format binaire utilise l'encodage mémoire de la donnée. Dans le mode binaire, si 123456 est un entier codé sur 4 octets, 4 octets du fichier sont utilisés pour encoder cette valeur. Le format binaire est utilisé pour compresser la taille du fichier en contrepartie il n'est pas lisible dans un éditeur de texte, on perd donc en souplesse. C'est souvent un format utilisé par certains programmes professionnels pour masquer les données. Surcharger l'opérateur << pour vos objets ----------------------------------------- Nous avons utilisé l'objet *cout* avec des types fondamentaux. Pour les structures, on peut afficher leur contenu en écrivant une longue ligne listant tous les éléments. Cette approche est peu lisible et doit être reproduite à chaque fois. Il est plus pratique de surcharger l'opérateur << pour qu'il gère les objets de votre type particulier. Ainsi, vous fixez un format d'affichage unique pour tous les objets de ce type. Voici un exemple dans le code ci-dessous : .. code-block:: cpp #include using namespace std; struct Point { int x,y; Point(int xx,int yy) { x = xx; y = yy; } }; ostream & operator << (ostream & stream, const Point & P) { stream << "(" << P.x << "," << P.y << ")"; return stream; } int main() { Point P(4,5); cout << P; } Stream bidirectionnelle ----------------------- Certains objets stream gèrent l'écriture et la lecture simultanément. Voici un exemple dans le code ci-dessous : .. code-block:: cpp #include // std::string #include // std::cout #include // std::stringstream using namespace std; int main () { std::stringstream ss; int a = 1; ss << a << a << a; // écriture de 1 1 1 int aa; ss >> aa; cout << aa; // aa = 111 }